package com.tong.hao.zuul.config.swagger;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import springfox.documentation.swagger2.web.Swagger2Controller;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * @ClassName ZuulSwagger2Config
 * @Author 友野浩二
 * @Description Swagger2配置
 * @Version 1.0
 * <pre>
 *      1- Swagger是一款RESTful接口的文档在线自动生成、功能测试功能框架。
 *          一个规范和完整的框架，用于生成、描述、调用和可视化RESTful风格的Web服务，加上swagger-ui，可以有很好的呈现。
 *          @EnableSwagger2 注解启用 Swagger2
 *      2- @Configuration用于定义配置类
 *          可替换xml配置文件，被注解的类内部包含有一个或多个被@Bean注解的方法，
 *          这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描，并用于构建bean定义，初始化Spring容器。
 *      3- SpringBoot集成Swagger2遇到异常：请求不到swagger-ui.html的问题
 *          这个错误，是因为静态资源路径映射问题导致。
 *          我们在访问http://localhost/swagger-ui.html时，这个swagger-ui.html相关的所有前端静态文件都在springfox-swagger-ui-2.8.0.jar/META-INF/resources里面。
 *          SpringBoot自动配置本身并不会把/swagger-ui.html这个路径映射到对应的目录META-INF/resources/下面,所以我们需要手动的将静态资源路径指向这里。
 *          【注】：解决方法参见WebMvcConfig类
 *      5- 继承SwaggerResourcesProvider接口是实现聚合api的关键，另外通过RouteLocator类获取路由列表是实现自动聚合的关键。
 *      【哈哈哈】: 这个类可以参考 springfox.documentation.swagger.web.InMemorySwaggerResourcesProvider
 * </pre>
 * <p>
 * //@Primary注解作用: 多个bean时 此类优先使用
 */
@EnableSwagger2
@Configuration
@Primary
@Slf4j
public class ZuulSwagger2Config implements SwaggerResourcesProvider {

    /**
     * 微服务的应用上下文
     */
    public static final String CONTEXT_PATH = "context-path";

    /**
     * 是否开启swagger2
     */
    @Value(value = "${swagger2.enabled}")
    Boolean swaggerEnabled;

    /**
     * 微服务应用名
     */
    @Value(value = "${spring.application.name}")
    String zuulApplicationName;

    /**
     * 路由
     */
    @Autowired
    RouteLocator routeLocator;

    /**
     * 服务发现
     */
    @Autowired
    private DiscoveryClient discoveryClient;

    /**
     * zuul配置信息
     */
    @Autowired
    private ZuulProperties zuulProperties;

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
                // 是否开启
                .enable(swaggerEnabled).select()
                // 扫码接口的包路径
                .apis(RequestHandlerSelectors.basePackage("com.tong.hao.zuul.controller"))
                // 指定路径处理PathSelectors.any()代表所有的路径
                .paths(PathSelectors.any())
                .build()
                .pathMapping("/");
    }

    /**
     * @return springfox.documentation.service.ApiInfo
     * @Author 友野浩二
     * @Description // apiInfo中，主要配置一下Swagger2文档网站的信息
     **/
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder().title("SpringBoot2.x集成Swagger2 Tong Zull 聚合 RESTful API")
                .description("Zuul聚合API描述")
                // 作者信息
                .contact(new Contact("ShiHao", "http://localhost:9211/swagger-ui.html", "15771691708@163.com"))
                // 版本
                .version("1.0")
                .build();
    }

    /**
     * 【方案一】: 利用routeLocator动态引入微服务
     * 【问题】: 各个微服务的swagger无法加载,因为各个微服务加了 server.servlet.context-path(应用上下文)
     * List<Route> routeList = this.routeLocator.getRoutes();
     * routeList.forEach(route -> {
     * //              微服务路由信息示例:
     * //              Route(org.springframework.cloud.netflix.zuul.filters.Route)
     * //                   {
     * //                       id='myTzac', fullPath='/sh/tzac/**', path='/**', location='tong-online-ac', prefix='/sh/tzac',
     * //                       retryable=false, sensitiveHeaders=[], customSensitiveHeaders=false, prefixStripped=true
     * //                   }
     * log.info("微服务路由信息: {}", route);
     * // 动态获取
     * swaggerResourceList.add(swaggerResource(route.getId(),
     * route.getFullPath().replace("/**", Swagger2Controller.DEFAULT_URL),
     * "2.0"));
     * });
     */
    @Override
    public List<SwaggerResource> get() {
        List<SwaggerResource> swaggerResourceList = new ArrayList<>();
        // 加入zuul自身的Restful API
        swaggerResourceList.add(swaggerResource(this.zuulApplicationName, Swagger2Controller.DEFAULT_URL, "2.0"));

        String zuulPrefix = this.zuulProperties.getPrefix();
        // 【方案二】: 使用DiscoveryClient获取微服务列表进行聚合(即获取eureka中的信息)
//        在各自微服务中将自身的 server.servlet.context-path 放入eureka的metadata-map中,然后zuul中聚合的时候从metadata-map中获取即可。
//        也就是各自微服务中配置 eureka.instance.metadata-map.context-path=${server.servlet.context-path}
        this.discoveryClient.getServices().forEach(serviceId -> {
            log.info("微服务serviceId: {}", serviceId);
            Optional<ServiceInstance> serviceInstanceOptional = this.discoveryClient.getInstances(serviceId).stream().findFirst();
            if (serviceInstanceOptional.isPresent()) {
                Map<String, String> metadata = serviceInstanceOptional.get().getMetadata();
                log.info("微服务配置的eureka.instance.metadata-map.context-path: {}", metadata.get("context-path"));
                if (metadata.containsKey(CONTEXT_PATH)) {
                    // location: /zuulPrefix/微服务服务名(spring.application.name)/微服务应用上下文(server.servlet.context-path)/"/v2/api-docs"
                    swaggerResourceList.add(swaggerResource(serviceId,
                            zuulPrefix.concat("/").concat(serviceId).concat(metadata.get("context-path")).concat(Swagger2Controller.DEFAULT_URL),
                            "2.0"));
                }
            } else {
                swaggerResourceList.add(swaggerResource(serviceId,
                        "/".concat(serviceId).concat(Swagger2Controller.DEFAULT_URL),
                        "2.0"));
            }
        });

        log.info("swaggerResourceList内容: {}", swaggerResourceList);
        return swaggerResourceList;
    }

    private SwaggerResource swaggerResource(String applicationName, String location, String swaggerVersion) {
        SwaggerResource swaggerResource = new SwaggerResource();
        swaggerResource.setName(applicationName);
        swaggerResource.setLocation(location);
        swaggerResource.setSwaggerVersion(swaggerVersion);
        return swaggerResource;
    }
}
